#include "PerspectiveCamera.h"

using namespace OBR;

PerspectiveCamera::PerspectiveCamera() : Camera("shaders/Camera/PerspectiveCamera.cu")
{
    this->program_desc.raygen.entryFunctionName = "__raygen__perspective_camera";
    this->program_desc.kind = OPTIX_PROGRAM_GROUP_KIND_RAYGEN;
    this->program_desc.flags = OPTIX_GEOMETRY_FLAG_NONE;
}

PerspectiveCamera::PerspectiveCamera(float3 _orig, float3 _lookat, float3 _up, // camera facing
                                     float _znear, float _zfar,                // z - range
                                     float _width,
                                     float _height, // OPT 1: frame size in world space
                                     float _fov,
                                     float _aspect_ratio // OPT 2: h - fov & aspect-ratio
                                     )
    : Camera("shaders/Camera/PerspectiveCamera.cu")
{
    if ((_width != -1 && _height != -1) || (_fov != -1 && _aspect_ratio != -1))
    {
        this->orig = _orig;
        this->lookat = _lookat;
        this->up = _up;
        this->znear = _znear;
        this->zfar = _zfar;
        this->width = _width;
        this->height = _height;
        this->fov = _fov;
        this->aspect_ratio = _aspect_ratio;
        this->should_calc_width_height = (_fov != -1 && _aspect_ratio != -1);
    }
    else
    {
        std::cerr << "You should specify at least one of [width & height] or [fov & "
                     "aspect-ratio]"
                  << std::endl;
        throw std::runtime_error("invalid params");
    }

    this->program_desc.raygen.entryFunctionName = "__raygen__perspective_camera";
}

PerspectiveCamera::~PerspectiveCamera()
{
}

void PerspectiveCamera::uploadSbtData()
{
    this->calculateParams();

    PerspectiveCameraData data;
    data.U = this->U;
    data.V = this->V;
    data.W = this->W;
    data.orig = this->orig;
    data.znear = this->znear;
    data.zfar = this->zfar;

    this->sbt_data.resize(sizeof(PerspectiveCameraData));
    this->sbt_data.upload(&data, sizeof(PerspectiveCameraData));
}

void PerspectiveCamera::calculateParams()
{
    // calculate UV
    this->cam_front = normalize(this->lookat - this->orig);
    this->cam_right = normalize(cross(this->cam_front, this->up));
    this->cam_up = normalize(cross(this->cam_right, this->cam_front));
    this->U = this->cam_right;
    this->V = -this->cam_up;

    // calculate width & height
    if (should_calc_width_height)
    {
        this->width = 2.0 * std::tan(this->fov / 180.0 * M_PI / 2.0) * znear;
        this->height = this->width / this->aspect_ratio;
    }

    // calculate direction to left top corner
    this->U *= width;
    this->V *= height;
    this->W = znear * this->cam_front - this->cam_right * this->width * 0.5 +
              this->cam_up * this->height * 0.5;
}

void PerspectiveCamera::_buildFromConfig(const Json::Value &config)
{
    // defalut values
    this->medium_idx = config.isMember("medium") ? config["medium"].asUInt() : -1;
    this->up = make_float3(config["up"][0].asFloat(), config["up"][1].asFloat(),
                           config["up"][2].asFloat());
    this->orig = make_float3(config["origin"][0].asFloat(), config["origin"][1].asFloat(),
                             config["origin"][2].asFloat());
    this->lookat = make_float3(config["lookat"][0].asFloat(), config["lookat"][1].asFloat(),
                               config["lookat"][2].asFloat());
    this->znear = config["znear"].asFloat();
    this->zfar = config["zfar"].asFloat();
    // optional values
    bool has_w = config.isMember("width");
    bool has_h = config.isMember("height");
    bool has_fov = config.isMember("fov");
    bool has_asp = config.isMember("aspect ratio");
    this->width = has_w ? config["width"].asFloat() : -1;
    this->height = has_h ? config["height"].asFloat() : -1;
    this->fov = has_fov ? config["fov"].asFloat() : -1;
    this->aspect_ratio = has_asp ? config["aspect ratio"].asFloat() : -1;
    if ((has_w && has_h) || (has_fov && has_asp))
    {
        this->should_calc_width_height = (has_fov && has_asp);
    }
    else
    {
        std::cerr << "You should specify at least one of [width & height] or [fov & "
                     "aspect-ratio]"
                  << std::endl;
        throw std::runtime_error("invalid params");
    }
}

void PerspectiveCamera::rotate(float2 deg)
{
    float r = length(this->orig - this->lookat);
    float x = r * cosf(deg.y) * cosf(deg.x);
    float y = r * cosf(deg.y) * sinf(deg.x);
    float z = r * sinf(deg.y);
    this->orig = this->lookat - x * this->cam_front + y * this->cam_right + z * this->cam_up;
    this->calculateParams();
    this->uploadSbtData();
}

void PerspectiveCamera::translate(float2 dir)
{
    this->orig += dir.x * this->cam_right + dir.y * this->cam_up;
    this->lookat += dir.x * this->cam_right + dir.y * this->cam_up;
    this->calculateParams();
    this->uploadSbtData();
}

void PerspectiveCamera::zoom(float dist)
{
    this->orig += normalize(this->lookat - this->orig) * dist;
    this->calculateParams();
    this->uploadSbtData();
}

OptixProgramGroupOptions &PerspectiveCamera::getProgramGroupOptions()
{
    return this->program_opt;
}

OptixProgramGroupDesc &PerspectiveCamera::getProgramGroupDesc()
{
    this->program_desc.raygen.module = this->cuda_module->optix_module;
    return this->program_desc;
}